--builder组件解析
--解析内容主要来源于ai+本人自己的理解，模型为谷歌的clude-3.7-sonnet-thinking，解析内容仅供参考，尤其是举例部分，切勿无脑照搬。如有错误，欢迎加入一起修改。
--当前文件解析版本为游戏版本dst_game_release_r664200(136445)




--组件名:builder
--功能简介:该组件主要用于管理角色建造能力的核心组件，负责处理配方解锁、材料检查、物品制作以及与科技树的交互。
--依赖文件：
--    techtree.lua：用于创建TechTree对象




--组件执行流程:
--1.初始化流程
    -- Builder组件初始化时设置基本属性，包括已解锁配方列表、可用科技树级别、临时科技加成等
    -- 初始化与制作相关的状态，如缓冲制作物品、当前使用的原型机等
    -- 将所有配方状态同步到客户端
--2.科技树评估流程
    -- 通过OnUpdate函数持续评估玩家周围环境
    -- 搜索周围一定范围内的原型机(prototyper)
    -- 更新可用科技树级别(包括基础科技树和临时加成)
    -- 更新工作站提供的特殊配方
    -- 当科技树变化时触发事件并同步到客户端
--3.配方解锁与访问流程
    -- 系统判断玩家是否可以使用配方的条件：
        -- 是否已解锁该配方
        -- 是否有足够的科技树级别
        -- 是否有特定角色标签或技能
        -- 是否在特定工作站附近
    -- 当满足条件时可以解锁新配方，并存储在recipes列表中
--4.材料检查流程
    -- 检查玩家物品栏是否有所需材料
    -- 检查角色资源(如生命值、理智值)是否满足需求
    -- 计算材料修改器影响下实际需要的材料数量
    -- 处理特殊情况如物品模仿者(itemmimic)检查
--5.制作执行流程
    -- 从物品栏移除所需材料
    -- 消耗所需的角色资源(如生命、理智)
    -- 根据配方创建物品
    -- 根据情况将物品给予玩家或放置在世界中
    -- 处理特殊情况，如自动装备、堆叠物品等
--6.建筑放置流程
    -- 对于需要放置的建筑，使用缓冲制作机制
    -- 先消耗材料并将配方标记为缓冲状态
    -- 等待玩家选择放置位置
    -- 在玩家确认位置后实际创建建筑
--7.特殊机制
    -- 饥饿建造者机制：建造可获得饥饿值影响
    -- 临时科技加成：使用后会消耗
    -- 免费建造模式：不消耗材料
    -- 原型机激活：研究新物品时激活原型机










local TechTree = require("techtree")


--参数:
--    self:Builder组件实例,必选参数
--    ingredientmod:number,材料消耗修正系数，必选参数，如0.75，则表示材料消耗为原本的75%
--返回值:
--    无
--描述：
-- 这个函数是一个回调函数，当Builder组件的ingredientmod属性被修改时会被调用。ingredientmod是一个材料修正系数，用于调整制作物品时所需材料的数量。
-- 函数首先通过断言检查ingredientmod是否为常量表INGREDIENT_MOD中定义的有效值，如果不是则会抛出错误。这确保了材料修正系数只能设置为游戏中预定义的特定值。
-- 然后，函数将这个新设置的ingredientmod值通过replica.builder组件同步到客户端，使得客户端UI能够正确显示制作物品所需的材料数量。
--实现过程是通过调用self.inst.replica.builder:SetIngredientMod(ingredientmod)将值传递给客户端。
local function oningredientmod(self, ingredientmod)
    assert(INGREDIENT_MOD[ingredientmod] ~= nil, "Ingredient mods restricted to certain values, see constants.lua INGREDIENT_MOD")
    self.inst.replica.builder:SetIngredientMod(ingredientmod)
end






--参数：
--    self:Builder组件实例,必选参数
--    freebuildmode:boolean,是否处于免费建造模式，必选参数，如true，则表示处于免费建造模式
--返回值:
--    无
--描述：
    -- 这个函数是一个回调函数，当Builder组件的freebuildmode属性被修改时会被调用。freebuildmode是一个布尔值，用于控制玩家是否处于免费建造模式（不消耗材料即可建造物品）。
    -- 函数将这个布尔值通过replica.builder组件同步到客户端，使客户端UI能够正确显示玩家当前是否处于免费建造模式。
    -- 实现过程是通过调用self.inst.replica.builder:SetIsFreeBuildMode(freebuildmode)将状态传递给客户端。
local function onfreebuildmode(self, freebuildmode)
    self.inst.replica.builder:SetIsFreeBuildMode(freebuildmode)
end





--参数：
--    self:Builder组件实例,必选参数
--    current_prototyper:Entity,当前原型机或者nil，必选参数，如nil，则表示没有当前原型机
--返回值:
--    无
--描述：
    -- 这个函数是一个回调函数，当Builder组件的current_prototyper属性被修改时会被调用。current_prototyper是一个引用，指向玩家当前正在使用的原型机（如科学机器、炼金引擎等）。
    -- 函数将这个原型机引用通过replica.builder组件同步到客户端，使客户端能够知道玩家当前正在使用哪个原型机，从而在UI中正确显示可研究的物品和科技树信息。
    -- 实现过程是通过调用self.inst.replica.builder:SetCurrentPrototyper(current_prototyper)将当前使用的原型机信息传递给客户端。
local function on_current_prototyper(self, current_prototyper)
	self.inst.replica.builder:SetCurrentPrototyper(current_prototyper)
end



--参数：无
--返回值：table，包含了与建造和技术树相关的设置项的方法映射，如{ingredientmod = oningredientmod, magic_bonus = function(self, bonus) ... end, ...}
--函数功能：创建并返回一个表，该表将作为 Builder 组件的元表属性。它定义了如何处理以下内容：
    -- 基础属性：
    -- ingredientmod：设置材料消耗修改器
    -- freebuildmode：设置自由建造模式
    -- current_prototyper：设置当前原型机
    -- 技术树奖励：
    -- 为 TechTree.BONUS_TECH 中的每个技术类型创建两种处理函数：
    -- 永久奖励函数（例如：science_bonus）
    -- 临时奖励函数（例如：science_tempbonus）
--实现流程：
    -- 初始化基础属性表
    -- 遍历所有技术树奖励类型
    -- 为每种技术添加永久和临时奖励的处理函数
    -- 返回完整的属性表
--游戏中的作用：
    --通过将属性与处理函数关联，确保当玩家获得或失去技术加成时，能正确更新客户端的建造界面和可建造物品列表。
local function metafn()
	local t =	{
		ingredientmod = oningredientmod,
		freebuildmode = onfreebuildmode,
		current_prototyper = on_current_prototyper,
	}
    for i, v in ipairs(TechTree.BONUS_TECH) do
        t[TechTree.AVAILABLE_TECH_BONUS[v] or string.lower(v).."_bonus"] = function(self, bonus) self.inst.replica.builder:SetTechBonus(string.lower(v), bonus) end
        t[TechTree.AVAILABLE_TECH_TEMPBONUS[v] or string.lower(v).."_tempbonus"] = function(self, bonus) self.inst.replica.builder:SetTempTechBonus(string.lower(v), (bonus or 0)) end
    end
	return t
end

local Builder = Class(function(self, inst)
    self.inst = inst    -- 包含该组件的实体实例，类型为 Entity 对象

    self.recipes = {}   --  玩家已解锁的配方列表，类型为表/数组
    self.station_recipes = {}   --  存储科技站提供的临时配方，键为配方名，值为true
    self.accessible_tech_trees = TechTree.Create()  --当前玩家可访问的科技树级别，包含临时科技加成
    self.accessible_tech_trees_no_temp = TechTree.Create()  --不包含临时科技加成的科技树级别
	self.old_accessible_tech_trees = {}  -- 表（哈希）,存储上一帧的科技树状态，用于检测变化
    self.inst:StartUpdatingComponent(self)  
    self.current_prototyper = nil   --Entity或nil,玩家当前使用的原型机/科技站实体
    self.buffered_builds = {}   -- 表（哈希），存储已准制作但尚未放置的建筑，键为配方名，值为true

    for i, v in ipairs(TechTree.BONUS_TECH) do
        self[TechTree.AVAILABLE_TECH_BONUS[v] or string.lower(v).."_bonus"] = 0
    end
    self.ingredientmod = 1  -- 材料消耗修饰符，影响制作物品时的材料消耗量,百分比形式，0.5表示材料消耗减半


    self.freebuildmode = false --是否处于自由建造模式（不消耗材料）

--1.科技树同步：
    --将当前角色的科技树状态(self.accessible_tech_trees)同步到网络复制组件(replica.builder)
    --这确保客户端UI能够正确显示玩家可以制作的物品
--2.重置所有Buffered Build状态：
    -- 遍历游戏中所有配方(AllRecipes)
    -- 对每个有效配方，将其"缓冲建造"状态设为false
    -- "缓冲建造"(Buffered Build)是指已消耗材料但尚未放置的建筑
    -- 初始化时所有配方都处于未缓冲状态
    self.inst.replica.builder:SetTechTrees(self.accessible_tech_trees)
    for k, v in pairs(AllRecipes) do
        if IsRecipeValid(v.name) then
            self.inst.replica.builder:SetIsBuildBuffered(v.name, false)
        end
    end



--建立了一个"排除标签"列表，用于筛选科技站：
--1.基础排除标签:
    -- "INLIMBO" - 不在游戏世界中活跃的实体(如背包中的物品)
    -- "fire" - 正在燃烧的物体
--2.角色特定排除：
    -- 遍历所有自定义制作选项卡(CUSTOM_RECIPETABS)
    -- 检查每个选项卡是否有特定的所有者标签(owner_tag)
    -- 如果该选项卡有所有者标签，但当前角色(inst)没有此标签
    -- 则将此标签添加到排除列表中
--3.功能:
    -- 这确保玩家在检测周围可用科技站时，只能使用与自己角色相匹配的科技站。
    --例如，如果某个制作选项卡需要"master_chef"标签，但玩家角色不是厨师，则带有此标签的科技站会被排除。
    -- 这种机制使游戏能够为不同角色提供专属的制作系统和科技站。
    self.exclude_tags = { "INLIMBO", "fire" } --查找原型机时排除的标签列表
    for k, v in pairs(CUSTOM_RECIPETABS) do
        if v.owner_tag ~= nil and not inst:HasTag(v.owner_tag) then
            table.insert(self.exclude_tags, v.owner_tag)
        end
    end
end,
nil,
metafn()
)



--参数:
--    recipe:table,配方对象,必选参数,无默认值,指定要研究的配方
--返回值:
--    无
--描述:
    -- 当玩家使用科学原型机（如科学机器、炼金引擎等）制作物品时，该函数会激活当前使用的原型机。
    -- 激活后，原型机会播放动画效果，可能产生粒子效果或声音，表示研究过程。
    -- 在饥荒中，不同的原型机有不同的激活效果，例如冒出电光，有魔法粒子等。
function Builder:ActivateCurrentResearchMachine(recipe)
    if self.current_prototyper ~= nil and
        self.current_prototyper.components.prototyper ~= nil and
        self.current_prototyper:IsValid() then
        self.current_prototyper.components.prototyper:Activate(self.inst, recipe)
    end
end



--参数:
--    inst:Entity,指定要检查距离的实体（通常是玩家）,必选参数,无默认值
--    last_hungry_build_pt:Point（坐标点，包含x,y,z值）,上一次触发饥饿建造的位置,必选参数,无默认值
--返回值:
--    boolean,返回玩家是否移动了足够远的距离
--描述:
    --这个函数用于检查玩家是否从上次触发"饥饿建造"效果的位置移动了足够远的距离。
    --在游戏中，某些角色有饥饿建造的特性（即建造回复饥饿值），如果玩家移动了足够远的距离，才能再次触发这个效果。
    --这是通过比较当前位置与记录位置的平方距离是否大于常量 TUNING.HUNGRY_BUILDER_RESET_DISTANCE_SQ 来实现的。
local function CheckHungryDistance(inst, last_hungry_build_pt)
    return inst:GetDistanceSqToPoint(last_hungry_build_pt) > TUNING.HUNGRY_BUILDER_RESET_DISTANCE_SQ
end



--参数:无
--返回值:
    --table,包含需要保存的建造者组件数据
--描述：
    -- 此函数在游戏保存时被调用，用于保存玩家的建造相关数据。它包括：
        -- 缓冲的建造物（已消耗材料但尚未放置的建筑）
        -- 已解锁的配方列表
        -- 饥饿建造相关的时间和位置数据
        -- 临时科技加成信息
    -- 这确保了玩家在下次加载游戏时能够恢复到保存前的建造状态。
function Builder:OnSave()
    local hungrytime = self.last_hungry_build ~= nil and math.ceil(GetTime() - self.last_hungry_build) or math.huge
    hungrytime = hungrytime < TUNING.HUNGRY_BUILDER_RESET_TIME and hungrytime or nil
    local hungrypt = self.last_hungry_build_pt and not CheckHungryDistance(self.inst, self.last_hungry_build_pt) and {x=self.last_hungry_build_pt.x, y=self.last_hungry_build_pt.y, z=self.last_hungry_build_pt.z} or nil

    local tempbonuses = self:GetTempTechBonuses()

    return
    {
        buffered_builds = self.buffered_builds,
        recipes = self.recipes,
        hungrytime = hungrytime,
        hungrypt = hungrypt,
        tempbonuses = tempbonuses,
        temptechbonus_count = self.temptechbonus_count,
    }
end





--参数:
--    data:table,从存档加载的数据表,必选参数,无默认值
--返回值:
--    无
--描述:
    -- 此函数在游戏加载存档时调用，用于从存档数据中恢复建造者组件的状态。处理过程包括：
        -- 恢复缓冲的建造物（已消耗材料但尚未放置的建筑）
        -- 恢复已解锁的配方列表
        -- 恢复饥饿建造相关的时间和位置数据
        -- 恢复临时科技加成
    -- 这个函数确保玩家在加载游戏后能够继续从上次保存的建造状态继续游戏，包括已学习的配方和准备建造的结构。
    -- 在游戏实现流程上，数据先通过OnSave函数保存到存档文件中，然后在加载游戏时通过OnLoad函数从存档中读取数据并应用到Builder组件上。
function Builder:OnLoad(data)
    if data.buffered_builds ~= nil then
        for k, v in pairs(AllRecipes) do
            if data.buffered_builds[k] ~= nil and IsRecipeValid(v.name) then
                self.inst.replica.builder:SetIsBuildBuffered(v.name, true)
                self.buffered_builds[k] = true
            end
        end
    end

    if data.recipes ~= nil then
        for i, v in ipairs(data.recipes) do
            if IsRecipeValid(v) then
                self:AddRecipe(v)
            end
        end
    end

    if data.hungrytime ~= nil then
        self.last_hungry_build = GetTime() - data.hungrytime
    end
    if data.hungrypt then
        self.last_hungry_build_pt = Point(data.hungrypt.x, data.hungrypt.y, data.hungrypt.z)
    end

    if data.tempbonuses then
        self:GiveTempTechBonus(data.tempbonuses)
        self.temptechbonus_count = data.temptechbonus_count
    end
end





--参数:
   --recname:string,表示配方名称,必选参数,无默认值
--返回值:
    --boolean,表示指定配方是否已缓存在建造缓冲区中
    --作用是告知游戏某个结构配方是否已经预先消耗了材料但尚未放置
--描述:
    --此函数检查指定的配方是否在建造缓冲区中。
    --在《饥荒》中，当玩家选择建造需要放置的结构（如篝火、科学机器等）时，游戏会先消耗材料并将该配方放入缓冲区，直到玩家选择合适位置放置该结构。
    --这个函数用于检查某个配方是否处于这种"已消耗材料但尚未放置"的状态。
--实现流程:
    -- 直接返回 self.buffered_builds[recname] 的值，该属性是一个哈希表，存储了所有已缓冲的配方
    -- 如果配方在缓冲区中存在并已标记为 true，则返回 true，否则返回 nil 或 false
function Builder:IsBuildBuffered(recname)
    return self.buffered_builds[recname]
end







--参数:无
--返回值:
    --无
--描述:
    -- 这是组件的更新函数，由游戏引擎定期调用。
    --其主要作用是持续评估玩家周围的科技树状态，检查玩家是否接近各种科研站（如科学机器、炼金引擎等），并相应地更新可用的科技等级。
--实现流程：
    -- 调用 self:EvaluateTechTrees()，这个方法会：
    -- 检测玩家周围一定范围内的所有科研站（prototyper）
    -- 更新玩家当前可用的科技等级
    -- 更新玩家可制作的配方列表
    -- 激活最近的科研站，关闭其他科研站
function Builder:OnUpdate()
    self:EvaluateTechTrees()
end




--参数:无
--返回值:
    --无
--描述:
    --这是一个作弊功能，用于切换免费建造模式的开关状态。
    --在免费建造模式下，玩家可以不消耗材料建造任何物品。
    --这通常用于测试或调试目的，不是正常游戏流程的一部分。
--实现流程：
    -- 将 self.freebuildmode 的值取反，从而切换免费建造模式的开/关状态
    -- 触发 "unlockrecipe" 事件，通知游戏界面更新可制作的配方列表
    -- 当 freebuildmode 为 true 时，HasIngredients 函数会直接返回 true，使玩家能够不消耗材料制作任何物品
function Builder:GiveAllRecipes()
    self.freebuildmode = not self.freebuildmode
    self.inst:PushEvent("unlockrecipe")
end



--参数:
    --recipetree:表格类型，必填，表示配方所需的科技等级，如{SCIENCE = 1, MAGIC = 0}
    --buildertree: 表格类型，必填，表示建造者当前拥有的科技等级，如{SCIENCE = 2, MAGIC = 1}
--返回值:
    --boolean,表示建造者的科技等级是否满足配方要求
--描述:
    --这个函数用于判断玩家的科技等级是否足以满足某个配方的科技要求。
    --游戏中的制作系统基于科技等级，不同的物品需要不同等级的科技才能制作
--实现流程：
    -- 遍历配方所需的各种科技类型及其等级 (recipetree)
    -- 对于每种科技类型，检查建造者拥有的等级 (buildertree) 是否大于或等于配方所需的等级
    -- 如果存在任何一种科技类型，建造者的等级低于配方所需等级，则返回 false
    -- 如果所有科技类型都满足要求，则返回 true
    -- 这个函数采用了严格检查方式，确保玩家具备所有必要的科技等级
local function propertech(recipetree, buildertree)
    for k, v in pairs(recipetree) do
        if buildertree[tostring(k)] ~= nil and
            recipetree[tostring(k)] ~= nil and
            recipetree[tostring(k)] > buildertree[tostring(k)] then
            return false
        end
    end
    return true
end




--参数:
    --tech:table类型，必选参数，无默认值。表示科技树信息，包含各种科技级别。例如: {SCIENCE = 1, MAGIC = 0}
--返回值:
    --无
--描述:
    --此函数用于解锁与指定科技等级相匹配的所有配方。
    --它会遍历游戏中的所有配方，检查每个配方是否有效，然后通过propertech函数判断该配方的科技要求是否符合提供的科技树级别。
    --如果满足条件，则调用UnlockRecipe解锁该配方。
function Builder:UnlockRecipesForTech(tech)
    for k, v in pairs(AllRecipes) do
        if IsRecipeValid(v.name) and propertech(v.level, tech) then
            self:UnlockRecipe(v.name)
        end
    end
end



--参数:无
--返回值:
    --table类型，返回包含所有科技加成的表格。例如{SCIENCE = 1, MAGIC = 2}
--描述:
    --此函数用于获取当前玩家拥有的所有科技加成,包括永久加成和临时加成
    --它遍历TechTree.BONUS_TECH中定义的所有科技类型，获取对应的永久加成值和临时加成值，并将它们相加后返回完整的加成信息。
function Builder:GetTechBonuses()
	local bonus = {}
    for i, v in ipairs(TechTree.BONUS_TECH) do
        bonus[v] = self[TechTree.AVAILABLE_TECH_BONUS[v] or string.lower(v).."_bonus"] or nil
        
        local tempbonus = self[TechTree.AVAILABLE_TECH_TEMPBONUS[v] or string.lower(v).."_tempbonus"]
        if tempbonus ~= nil then
            if bonus[v] ~= nil then
                bonus[v] = bonus[v] + tempbonus
            else
                bonus[v] = tempbonus
            end
        end
    end

	return bonus
end



--参数:无
--返回值:
    --table类型，返回只包含临时科技加成的表格。
--描述:
    -- 此函数只收集玩家当前拥有的临时科技加成，不包括永久加成。
    --它遍历TechTree.BONUS_TECH中定义的所有科技类型，获取对应的临时加成值，如果存在则添加到返回的表格中。
function Builder:GetTempTechBonuses()
    local bonus = {}
    for i, v in ipairs(TechTree.BONUS_TECH) do
        
        local tempbonus = self[TechTree.AVAILABLE_TECH_TEMPBONUS[v] or string.lower(v).."_tempbonus"]
        if tempbonus ~= nil then
            bonus[v] = tempbonus
        end
    end

	return bonus
end



--参数:
    --tech:table类型，必选参数，无默认值。表示要添加的临时科技加成。，包含各种科技类型及其对应的加成值。例如: {SCIENCE = 1, MAGIC = 2}
--返回值:
    --无
--描述:
    --此函数给玩家添加临时科技加成。它遍历传入的tech表格，将各项科技加成赋值给玩家的对应临时加成属性。
    --同时，它会增加临时加成计数器temptechbonus_count，确保多个来源的临时加成可以正确堆叠和移除。
function Builder:GiveTempTechBonus(tech)
    for k, v in pairs(tech) do
        self[TechTree.AVAILABLE_TECH_TEMPBONUS[k] or string.lower(k).."_tempbonus"] = v
    end

    if self.temptechbonus_count ~= nil then
        self.temptechbonus_count = self.temptechbonus_count + 1
    else
        self.temptechbonus_count = 1
    end
end



--参数:无
--返回值:
    --无
--描述:
    --此函数用于消耗临时科技加成。
    --它首先检查临时加成计数器是否存在，如果不存在则直接返回。
    --如果存在，则将计数器减1，当计数器降至0时，清除玩家所有的临时科技加成并重置计数器为nil。
function Builder:ConsumeTempTechBonuses()
	if self.temptechbonus_count == nil then
		--we should NOT reach here normally; only assert in dev branch though!
		assert(BRANCH ~= "dev")
        return
    end

    self.temptechbonus_count = self.temptechbonus_count - 1
    if self.temptechbonus_count < 1 then
        for i, v in ipairs(TechTree.BONUS_TECH) do
            if self[TechTree.AVAILABLE_TECH_TEMPBONUS[v] or string.lower(v).."_tempbonus"] ~= nil then
                self[TechTree.AVAILABLE_TECH_TEMPBONUS[v] or string.lower(v).."_tempbonus"] = nil
            end
        end

        self.temptechbonus_count = nil
    end
end


--参数:
    --src:table类型，必选参数，无默认值。作为复制操作的源数据，包含各种技术类型及其对应等级。例如: {SCIENCE = 1, MAGIC = 2}
    --dest:table类型，必选参数，无默认值。接收从源技术树复制的数据，用于存储复制后的科技加成值。
--返回值:
    --无
--描述:
    --遍历所有可用的技术类型(TechTree.AVAILABLE_TECH数组)
    -- 对于每种技术类型，将源技术树中的等级值复制到目标技术树中
    -- 如果源技术树中没有特定技术的等级值，则在目标技术树中设置为0
--实现流程:
    -- 函数接收两个参数：源技术树和目标技术树
    -- 使用for循环遍历TechTree.AVAILABLE_TECH中的所有技术类型
    -- 对于每种技术类型，从源树中读取对应值(或默认为0)并写入目标树
    -- 完成遍历后，目标技术树包含与源技术树相同的技术等级数据
local function CopyTechTrees(src, dest)
	for i, v in ipairs(TechTree.AVAILABLE_TECH) do
		dest[v] = src[v] or 0
	end
end

local PROTOTYPER_TAGS = { "prototyper" }

--参数:无
--返回值:
    --无
--描述和流程:
    --这个函数在《饥荒》中用于定期检查玩家周围环境，自动更新可用技术等级，主要流程如下：
        -- 1.检测玩家周围的原型机（科学机器、炼金引擎等）
            -- 如果有override_current_prototyper（手动指定的原型机），会优先考虑
            -- 否则搜索周围TUNING.RESEARCH_MACHINE_DIST范围内的所有带"prototyper"标签的实体
        -- 2.更新技术树
            -- 保存旧的技术树状态以便后续比较
            -- 重置当前的原型机和工作站配方
        -- 3.激活最近的原型机
            -- 只激活找到的第一个可用原型机（距离最近的）
            -- 获取该原型机提供的技术等级
            -- 收集该原型机工作站提供的独特配方（nounlock标记的配方）
        -- 4.计算最终技术等级
            -- 合并角色自身的技术加成（永久加成和临时加成）
            -- 更新accessible_tech_trees和accessible_tech_trees_no_temp
        -- 5.处理配方变化
            -- 比较新旧工作站配方，更新replica中的配方列表
            -- 检查技术树是否有变化
        -- 6.通知系统技术树更新
            -- 如果技术树有变化，触发"techtreechange"事件
            -- 同步更新客户端技术树状态
        -- 7.处理界面更新
            -- 如果使用了手动指定的原型机，可能会打开制作菜单
function Builder:EvaluateTechTrees()
    local pos = self.inst:GetPosition()

    local ents
	if self.override_current_prototyper then
		if self.override_current_prototyper:IsValid() 
			and self.override_current_prototyper:HasTags(PROTOTYPER_TAGS) 
			and not self.override_current_prototyper:HasOneOfTags(self.exclude_tags)
			and (self.override_current_prototyper.components.prototyper.restrictedtag == nil or self.inst:HasTag(self.override_current_prototyper.components.prototyper.restrictedtag))
			and self.inst:IsNear(self.override_current_prototyper, TUNING.RESEARCH_MACHINE_DIST)
			then

			ents = {self.override_current_prototyper}
		else
			self.override_current_prototyper = nil
		end
	end
	
	if ents == nil then
		ents = TheSim:FindEntities(pos.x, pos.y, pos.z, TUNING.RESEARCH_MACHINE_DIST, PROTOTYPER_TAGS, self.exclude_tags)
	end

	CopyTechTrees(self.accessible_tech_trees, self.old_accessible_tech_trees)
    local old_station_recipes = self.station_recipes
    local old_prototyper = self.current_prototyper
    self.current_prototyper = nil
    self.station_recipes = {}

    local prototyper_active = false
    for i, v in ipairs(ents) do
        if v.components.prototyper ~= nil and (v.components.prototyper.restrictedtag == nil or self.inst:HasTag(v.components.prototyper.restrictedtag)) then
            if not prototyper_active then
                --activate the first machine in the list. This will be the one you're closest to.
                v.components.prototyper:TurnOn(self.inst)

				--prototyper:GetTrees() returns a deepcopy, which we no longer want
				CopyTechTrees(v.components.prototyper.trees, self.accessible_tech_trees)

                if v.components.craftingstation ~= nil then
                    local recs = v.components.craftingstation:GetRecipes(self.inst)
                    for _, recname in ipairs(recs) do
						local recipe = GetValidRecipe(recname)
                        if recipe ~= nil and recipe.nounlock then
                            --only nounlock recipes can be unlocked via crafting station
                            self.station_recipes[recname] = true
						end
                    end
				end

                prototyper_active = true
                self.current_prototyper = v
            else
                --you've already activated a machine. Turn all the other machines off.
                v.components.prototyper:TurnOff(self.inst)
            end
        end
    end

    --V2C: Hacking giftreceiver logic in here so we do
    --     not have to duplicate the same search logic
    if self.inst.components.giftreceiver ~= nil then
        self.inst.components.giftreceiver:SetGiftMachine(
            self.current_prototyper ~= nil and
            self.current_prototyper:HasTag("giftmachine") and
            CanEntitySeeTarget(self.inst, self.current_prototyper) and
            self.inst.components.inventory.isopen and --ignores .isvisible, as long as it's .isopen
            self.current_prototyper or
            nil)
    end

    --add any character specific bonuses to your current tech levels.
	CopyTechTrees(self.accessible_tech_trees, self.accessible_tech_trees_no_temp)
    if not prototyper_active then
        for i, v in ipairs(TechTree.AVAILABLE_TECH) do
            self.accessible_tech_trees_no_temp[v] = (self[TechTree.AVAILABLE_TECH_BONUS[v] or string.lower(v).."_bonus"] or 0)
            self.accessible_tech_trees[v] = (self[TechTree.AVAILABLE_TECH_TEMPBONUS[v] or string.lower(v).."_tempbonus"] or 0) + (self[TechTree.AVAILABLE_TECH_BONUS[v] or string.lower(v).."_bonus"] or 0)
        end
    else
		for i, v in ipairs(TechTree.BONUS_TECH) do
            self.accessible_tech_trees_no_temp[v] = self.accessible_tech_trees_no_temp[v] + (self[TechTree.AVAILABLE_TECH_BONUS[v] or string.lower(v).."_bonus"] or 0)
			self.accessible_tech_trees[v] = self.accessible_tech_trees[v] + 
                                            (self[TechTree.AVAILABLE_TECH_TEMPBONUS[v] or string.lower(v).."_tempbonus"] or 0) + 
                                            (self[TechTree.AVAILABLE_TECH_BONUS[v] or string.lower(v).."_bonus"] or 0)
		end
	end

    if old_prototyper ~= nil and
        old_prototyper ~= self.current_prototyper and
        old_prototyper.components.prototyper ~= nil and
        old_prototyper.entity:IsValid() then
        old_prototyper.components.prototyper:TurnOff(self.inst)
    end

    local trees_changed = false

    for recname, _ in pairs(self.station_recipes) do
        if old_station_recipes[recname] then
            old_station_recipes[recname] = nil
        else
            self.inst.replica.builder:AddRecipe(recname)
            trees_changed = true
        end
    end

    if next(old_station_recipes) ~= nil then
        for recname, _ in pairs(old_station_recipes) do
            self.inst.replica.builder:RemoveRecipe(recname)
        end
		trees_changed = true
    end

    if not trees_changed then
        for k, v in pairs(self.old_accessible_tech_trees) do
            if v ~= self.accessible_tech_trees[k] then
                trees_changed = true
                break
            end
        end
		--V2C: not required anymore; both trees should have the same keys now
        --[[if not trees_changed then
            for k, v in pairs(self.accessible_tech_trees) do
                if v ~= self.old_accessible_tech_trees[k] then
                    trees_changed = true
                    break
                end
            end
        end]]
    end

    if trees_changed then
        self.inst:PushEvent("techtreechange", { level = self.accessible_tech_trees })
        self.inst.replica.builder:SetTechTrees(self.accessible_tech_trees)
    end

	if self.override_current_prototyper ~= nil then
		if self.override_current_prototyper ~= self.current_prototyper then
			self.override_current_prototyper = nil
		elseif self.override_current_prototyper ~= old_prototyper then
			self.inst.replica.builder:OpenCraftingMenu()
		end
	end
end


--参数:
    --prototyper:实体(Entity)，可选参数，无默认值。指定要交互的原型机
--返回值:
    --布尔值, 字符串，返回是否成功使用原型机，以及可能的失败原因
--描述和流程:
    --该函数允许玩家允许玩家通过操作（如点击）特定原型机打开对应的制作界面。主要流程：
        -- 1.验证原型机有效性
            -- 检查原型机是否有"prototyper"标签
            -- 确保没有exclude_tags指定的排除标签
            -- 检查restrictedtag（如果原型机有限制标签，确保玩家有这个标签）
        -- 2.设置override_current_prototyper
            -- 如果验证通过，将该原型机设为override_current_prototyper
            -- 这会在下次EvaluateTechTrees调用时优先使用此原型机
        -- 3.打开制作菜单
            --如果指定的原型机已经是当前活跃的原型机，直接打开制作菜单
        -- 4.返回结果
            -- 成功时返回true
            -- 失败时返回false和失败原因。
function Builder:UsePrototyper(prototyper)
	if prototyper ~= nil then
		if not prototyper:HasTags(PROTOTYPER_TAGS) 
			or prototyper:HasOneOfTags(self.exclude_tags)
			or (prototyper.components.prototyper ~= nil and prototyper.components.prototyper.restrictedtag ~= nil and not self.inst:HasTag(prototyper.components.prototyper.restrictedtag))
			then

			local fail_str = prototyper.components.prototyper and prototyper.components.prototyper.restrictedtag or nil
			return false, fail_str ~= nil and string.upper(fail_str) or nil
		end
	end

	self.override_current_prototyper = prototyper
	if prototyper ~= nil and prototyper == self.current_prototyper then
		self.inst.replica.builder:OpenCraftingMenu()
	end
	return true
end



--参数:
    --recname:字符串类型，表示要添加的配方名称，必需参数，没有默认值
--返回值:
    --无
--功能：
    --将配方添加到玩家已知的配方列表中
--描述和流程:
    --1.检查配方是否已经在玩家的recipes列表中，如果不在则添加
    --2.通知玩家的replica组件添加这个配方（这是为了在客户端也能看到这个变化）
function Builder:AddRecipe(recname)
    if not table.contains(self.recipes, recname) then
        table.insert(self.recipes, recname)
    end
    self.inst.replica.builder:AddRecipe(recname)
end



--参数:
    --recname:字符串类型，表示要移除的配方名称，必需参数，没有默认值
--返回值:
    --无
--功能：
    --从玩家已知的配方列表中移除指定配方
--描述和流程：
    -- 使用table.removearrayvalue从玩家的recipes列表中移除指定配方
    -- 通知玩家的replica组件移除这个配方（这是为了在客户端也能看到这个变化）
function Builder:RemoveRecipe(recname)
	table.removearrayvalue(self.recipes, recname)
	self.inst.replica.builder:RemoveRecipe(recname)
end




--参数:
    --recname:字符串类型，指定要解锁的配方名称，必需参数，没有默认值
--返回值:
    --无
--功能：
    --用于解锁一个新的制作配方，让玩家能够制作该物品。
    --解锁配方时会给予玩家一些理智值奖励，并触发相关事件。
--流程:
    --1.通过GetValidRecipe(recname)获取有效的配方对象
    -- 2.检查配方是否存在且不是标记为nounlock的配方（nounlock配方通常是临时制作站提供的配方，不会被永久解锁）
    -- 3.如果玩家有理智组件，给予玩家中等量的理智值奖励（TUNING.SANITY_MED）
    -- 4.调用self:AddRecipe(recname)将配方添加到玩家的配方列表中
    -- 5.触发"unlockrecipe"事件，通知游戏其他部分玩家已解锁新配方
function Builder:UnlockRecipe(recname)
    local recipe = GetValidRecipe(recname)
    if recipe ~= nil and not recipe.nounlock then
    --print("Unlocking: ", recname)
        if self.inst.components.sanity ~= nil then
            self.inst.components.sanity:DoDelta(TUNING.SANITY_MED)
        end
        self:AddRecipe(recname)
        self.inst:PushEvent("unlockrecipe", { recipe = recname })
    end
end


--参数:
    --ingredients:table类型，必需参数，没有默认值，包含制作物品所需的材料集合
--返回值:
    --number，表示所有材料的平均湿度值
--功能：
    --计算制作材料的总湿度值，这个湿度值会传递给新制作的物品。
--描述和流程：
    -- 1.创建一个空数组来存储每个材料的湿度信息
    -- 2.遍历所有材料，检查每个物品的湿度
        -- 如果物品有inventoryitem组件，则获取其水分值
        -- 如果物品没有rainimmunity组件，则使用世界当前湿度值
        -- 否则使用0
    -- 3.计算所有材料的加权平均湿度：总湿度除以总物品数
    -- 4.如果没有物品，则返回0
function Builder:GetIngredientWetness(ingredients)
    local wetness = {}
    for item, ents in pairs(ingredients) do
        for k, v in pairs(ents) do
            table.insert(wetness,
            {
				wetness =
					(k.components.inventoryitem ~= nil and k.components.inventoryitem:GetMoisture()) or
					(k.components.rainimmunity == nil and TheWorld.state.wetness) or
					0,
                num = v,
            })
        end
    end

    local totalWetness = 0
    local totalItems = 0
    for k,v in pairs(wetness) do
        totalWetness = totalWetness + (v.wetness * v.num)
        totalItems = totalItems + v.num
    end

    return totalItems > 0 and totalWetness or 0
end




--参数:
    --recname: string类型，必需参数，没有默认值，表示要制作的配方名称
--返回值:
    --1.table，包含所有需要的材料及其数量,如{log = {item1=2, item2=1}, rocks = {item3=3}}
    --2.boolean，表示是否应用了材料折扣
--功能：
    --从玩家的库存中获取制作某个物品所需的所有材料
--流程：
    -- 1.查找配方表中指定名称的配方
    -- 2.创建一个空表来存储材料
    -- 3.遍历配方中的每种材料
    -- 4.计算实际需要的数量，考虑玩家的ingredientmod（材料修饰符）
    -- 5.从玩家库存中获取足够数量的该类型材料
    -- 6.记录是否应用了折扣（实际使用量小于配方原始量）
    -- 7.返回材料表和折扣标志
function Builder:GetIngredients(recname)
    local recipe = AllRecipes[recname]
    if recipe then
        local ingredients = {}
		local discounted = false
        for k,v in pairs(recipe.ingredients) do
			if v.amount > 0 then
				local amt = math.max(1, RoundBiasedUp(v.amount * self.ingredientmod))
				local items = self.inst.components.inventory:GetCraftingIngredient(v.type, amt)
				ingredients[v.type] = items
				if amt < v.amount then
					discounted = true
				end
			end
        end
        return ingredients, discounted
    end
end




--参数:
    --ingredients:table类型，必需参数，没有默认值，包含制作物品所需的材料集合
--返回值:
    --boolean，表示材料中是否包含模仿物（mimic）,true表示找到了模仿物，false表示没有模仿物
--功能：
    --检查玩家准备使用的材料中是否有模仿物，如果有则将其转变为邪恶状态。
--流程：
    -- 1. 遍历所有准备使用的材料
    -- 2.检查每个物品是否具有itemmimic组件
    -- 3.如果找到带有该组件的物品，将其转为邪恶状态
    -- 4.返回是否找到模仿物的结果
function Builder:CheckIngredientsForMimic(ingredients)
    for _, ents in pairs(ingredients) do
        for item in pairs(ents) do
            if item.components.itemmimic then
                item.components.itemmimic:TurnEvil(self.inst)
                return true
            end
        end
    end

    return false
end




--参数:
    --ingredients:table类型，必需参数，没有默认值，包含制作物品所需的材料集合
    --recname: string类型，必需参数，没有默认值，表示配方名称
    --discounted: boolean类型，可选参数，表示是否已应用折扣
--返回值:
    --无
--功能：
    --从玩家库存中移除制作物品所需的材料，并处理角色特定的消耗（如生命值、理智值等）。
--流程：
    -- 1.如果玩家处于自由建造模式，直接返回不消耗材料
    -- 2.遍历所有材料，从玩家库存中移除相应数量
        -- 如果移除的物品是容器，将其中的所有物品掉落在地上
        -- 删除移除的物品
    -- 3.查找配方并处理角色特定的消耗：
        -- 如果需要消耗生命值，减少玩家生命值
        -- 如果需要消耗最大生命值，增加玩家生命值惩罚
        -- 如果需要消耗理智值，减少玩家理智值
        -- 最大理智值的消耗是特殊处理的，官方有注释，详细可以看具体函数内容
    -- 4.触发"consumeingredients"事件，通知游戏系统材料已被消耗
function Builder:RemoveIngredients(ingredients, recname, discounted)
	if self.freebuildmode then
		return
	end

    for item, ents in pairs(ingredients) do
        for k,v in pairs(ents) do
            for i = 1, v do
                local item = self.inst.components.inventory:RemoveItem(k, false, true)

                -- If the item we're crafting with is a container,
                -- drop the contained items onto the ground.
                if item.components.container ~= nil then
                    item.components.container:DropEverything(self.inst:GetPosition())
                end

                item:Remove()
            end
        end
    end

    local recipe = AllRecipes[recname]
    if recipe then
        for k,v in pairs(recipe.character_ingredients) do
            if v.type == CHARACTER_INGREDIENT.HEALTH then
                self.inst:PushEvent("consumehealthcost")
                self.inst.components.health:DoDelta(-v.amount, false, "builder", true, nil, true)
            elseif v.type == CHARACTER_INGREDIENT.MAX_HEALTH then
                self.inst:PushEvent("consumehealthcost")
                self.inst.components.health:DeltaPenalty(v.amount)
            elseif v.type == CHARACTER_INGREDIENT.SANITY then
                self.inst.components.sanity:DoDelta(-v.amount)
            elseif v.type == CHARACTER_INGREDIENT.MAX_SANITY then
                --[[
                    Because we don't have any maxsanity restoring items we want to be more careful
                    with how we remove max sanity. Because of that, this is not handled here.
                    Removal of sanity is actually managed by the entity that is created.
                    See maxwell's pet leash on spawn and pet on death functions for examples.

					Note: Make sure you handle self.freebuildmode in this case

                    --翻译：
                    --游戏中没有恢复最大理智值的物品，因此需要更谨慎地处理最大理智值的减少。
                    --与生命值和普通理智值不同，最大理智值的扣除不是在制作时直接扣除的，而是由制作出的实体（entity）在创建或使用时负责处理。
                    --麦斯威尔（Maxwell）的宠物皮带（pet leash）就是一个例子，它在生成时和宠物死亡时会管理最大理智值的消耗。
                    --在处理最大理智值消耗时，需要特别注意检查self.freebuildmode（免费建造模式）状态，确保在此模式下不会消耗最大理智值。
                --]]
            end
        end
    end
    self.inst:PushEvent("consumeingredients", { discounted = discounted })
end








--参数:
    --ingredient: 表格(table)类型，必需参数，无默认值。表示制作配方所需的角色属性消耗，包含类型和数量信息,例如: {type = CHARACTER_INGREDIENT.HEALTH, amount = 30}
--返回值:
    --1. 布尔(boolean)类型，表示玩家是否满足该属性消耗的需求。
    --2.数字(number)类型，表示玩家当前拥有的相应属性值或可用数量。
--功能：
    --检查玩家角色是否满足制作某配方所需的特殊属性消耗，如生命值、最大生命值、理智值或最大理智值。
--流程：
    -- 1. 根据传入的ingredient.type判断检查哪种角色属性
    -- 2. 对于生命值(HEALTH)：
        -- 检查角色是否有健康组件
        -- 计算所需数量，考虑老年特性的缩放
        -- 返回当前生命值是否足够以及当前生命值数量
    -- 3.对于最大生命值(MAX_HEALTH)：
        -- 检查当前生命值减益是否允许进一步减少
        -- 返回是否可以扣减以及当前可用值
    -- 4.对于理智值(SANITY)：
        -- 类似生命值的检查
    -- 5. 对于最大理智值(MAX_SANITY)：
        -- 类似最大生命值的检查
    -- 6.默认返回false, 0表示不满足条件
function Builder:HasCharacterIngredient(ingredient)
    if ingredient.type == CHARACTER_INGREDIENT.HEALTH then
        if self.inst.components.health ~= nil then
            --round up health to match UI display
			local amount_required = self.inst:HasTag("health_as_oldage") and math.ceil(ingredient.amount * TUNING.OLDAGE_HEALTH_SCALE) or ingredient.amount
            local current = math.ceil(self.inst.components.health.currenthealth)
            return current > amount_required, current --Don't die from crafting!
        end
    elseif ingredient.type == CHARACTER_INGREDIENT.MAX_HEALTH then
        if self.inst.components.health ~= nil then
            local penalty = self.inst.components.health:GetPenaltyPercent()
            return penalty + ingredient.amount <= TUNING.MAXIMUM_HEALTH_PENALTY, 1 - penalty
        end
    elseif ingredient.type == CHARACTER_INGREDIENT.SANITY then
        if self.inst.components.sanity ~= nil then
            --round up sanity to match UI display
            local current = math.ceil(self.inst.components.sanity.current)
            return current >= ingredient.amount, current
        end
    elseif ingredient.type == CHARACTER_INGREDIENT.MAX_SANITY then
        if self.inst.components.sanity ~= nil then
            local penalty = self.inst.components.sanity:GetPenaltyPercent()
            return penalty + ingredient.amount <= TUNING.MAXIMUM_SANITY_PENALTY, 1 - penalty
        end
    end
    return false, 0
end





--参数:
    --ingredient: 表格(table)类型，必需参数，无默认值。表示制作配方所需的科技等级要求。
--返回值:
    --1. 布尔(boolean)类型，表示玩家是否满足科技等级要求。
    --2. 数字(number)类型，表示玩家当前的科技等级。
--功能：
    --检查玩家是否满足制作某配方所需的科技等级要求。
--流程：
    -- 1. 检查输入的ingredient.type是否是科技类型，并且以"material"结尾
    -- 2. 如果满足条件：
        -- 提取科技类型名称（移除"material"后缀）并转为大写
        -- 从玩家的可访问科技树中获取相应的科技等级
        -- 比较玩家的科技等级是否大于等于所需等级
    -- 3.返回比较结果和玩家当前的科技等级
    -- 4.如果不是科技类型，则返回false, 0
function Builder:HasTechIngredient(ingredient)
    if IsTechIngredient(ingredient.type) and ingredient.type:sub(-9) == "_material" then
        local level = self.accessible_tech_trees[ingredient.type:sub(1, -10):upper()] or 0
        return level >= ingredient.amount, level
    end
    return false, 0
end




--参数:
    --recipe: 表格(table)类型，必需参数，无默认值。包含配方的所有信息，如材料、结果等。
    --pt: Vector3类型，可选参数，默认为角色当前位置。表示制作物品的目标位置。
    --rot: 数字(number)类型，可选参数，无默认值。表示放置物品的旋转角度。
    --skin: 字符串(string)类型，可选参数，无默认值。表示物品的皮肤ID。
    --onsuccess: 函数(function)类型，可选参数，无默认值。制作成功后的回调函数。
--返回值:
    --布尔(boolean)类型，表示配方是否制作成功。
--功能：
    --触发物品制作过程，包括检查条件、开始制作动作以及设置回调。
--流程：
    -- 1. 检查配方是否有效，以及角色不在溺水或坠落状态
    -- 2. 触发"makerecipe"事件
    -- 3. 检查是否有缓冲构建或满足材料需求
    -- 4. 停止角色的移动
    -- 5. 创建BUILD动作的缓冲动作对象
    -- 6. 设置动作的皮肤和成功回调
    -- 7. 将动作推送到角色的移动控制器中
    -- 8. 返回是否成功启动制作过程
function Builder:MakeRecipe(recipe, pt, rot, skin, onsuccess)
    if recipe ~= nil and not self.inst.sg:HasStateTag("drowning") and not self.inst.sg:HasStateTag("falling") then -- TODO(JBK): Check if "drowning" can be replaced with "busy" instead with no side effects.
        self.inst:PushEvent("makerecipe", { recipe = recipe })
        if self:IsBuildBuffered(recipe.name) or self:HasIngredients(recipe) then
            self.inst.components.locomotor:Stop()
            local buffaction = BufferedAction(self.inst, nil, ACTIONS.BUILD, nil, pt or self.inst:GetPosition(), recipe.name, recipe.build_distance, nil, rot)
            buffaction.skin = skin
            if onsuccess ~= nil then
                buffaction:AddSuccessAction(onsuccess)
            end
            self.inst.components.locomotor:PushAction(buffaction, true)
            return true
        end
    end
    return false
end




--参数:
    --self: Builder组件实例，必需参数，无默认值。
    --recipe: 表格(table)类型，必需参数，无默认值。包含配方信息。
    --item: 实体(Entity)类型，必需参数，无默认值。表示要给予或丢弃的物品。
    --pt:  Vector3类型，必需参数，无默认值。表示给予或丢弃物品的位置。
--返回值:
    --无
--功能：
    --根据配方设置决定将制作出的物品直接放入玩家背包，或者丢在地上。
--流程：
    -- 1.检查配方是否设置了dropitem属性
    -- 2.如果需要丢弃物品：
        -- 计算丢弃角度（基于角色朝向角度加上随机偏移）
        -- 计算丢弃位置（考虑物品和角色的物理半径，确保不重叠）
        -- 设置物品位置并调用物品的inventoryitem组件的OnDropped方法
    -- 3.否则，直接将物品添加到角色的物品栏中
local function GiveOrDropItem(self, recipe, item, pt)
	if recipe.dropitem then
		local angle = (self.inst.Transform:GetRotation() + GetRandomMinMax(-65, 65)) * DEGREES
		local r = item:GetPhysicsRadius(0.5) + self.inst:GetPhysicsRadius(0.5) + 0.1
		item.Transform:SetPosition(pt.x + r * math.cos(angle), pt.y, pt.z - r * math.sin(angle))
		item.components.inventoryitem:OnDropped()
	else
	    self.inst.components.inventory:GiveItem(item, nil, pt)
	end
end





--参数:
    --recname: 字符串(string)类型，必需参数，无默认值。表示指定要建造的配方名称。
    --pt: Vector3类型，可选参数，默认为当前玩家位置。指定放置物品/结构的位置坐标。
    --rotation: 数字(number)类型，可选参数，默认值0。设置放置物品/结构的旋转角度。
    --skin: 字符串(string)类型，可选参数，无默认值。指定物品的皮肤名称
--返回值:
    --布尔(boolean)类型，表示配方是否制作成功。
    --string，如果参数1为false，则需要该返回值，表示失败原因，用于触发角色对话提示。
--功能：
    --建造物品与结构的核心实现
--流程：
-- 1.验证前置条件：
    -- 检查配方是否有效，玩家是否有材料或已缓存建造
    -- 检查是否允许使用当前皮肤
    -- 检查特殊条件（如骑乘状态下不能放置某些物品）
    -- 检查是否需要特定工作台且工作台是否有效
    -- 执行配方可能包含的自定义建造条件
-- 2.处理饥饿建造者特性：
    -- 检查玩家是否有"饥饿建造者"标签
    -- 根据时间和移动距离条件给予饥饿度影响
-- 3.处理制造站类型的建造：
    -- 如果是制造站建造，移除材料后直接返回成功
    -- 制造站自己负责处理产物的生成
-- 4.生成物品实例：
    -- 使用SpawnPrefab创建物品实例
    -- 应用玩家指定的皮肤或配方默认皮肤
-- 5.处理不同类型的物品：
    -- 对于背包物品：
        -- 检查材料中是否包含拟态物品
        -- 继承材料的湿度
        -- 移除材料
        -- 触发相关事件
        -- 根据配方决定给予数量
        -- 处理可装备物品的装备逻辑
        -- 处理可堆叠物品的堆叠逻辑
    -- 对于放置型结构：
        -- 移除材料
        -- 设置放置位置和角度
        -- 触发建造事件和通知
-- 6.触发后续回调：
    -- 调用自定义onBuild回调
    -- 调用物品的OnBuilt方法
    -- 更新玩家建造统计数据
function Builder:DoBuild(recname, pt, rotation, skin)
    local recipe = GetValidRecipe(recname)
    if recipe ~= nil and (self:IsBuildBuffered(recname) or self:HasIngredients(recipe)) and not PREFAB_SKINS_SHOULD_NOT_SELECT[skin] then
        if recipe.placer ~= nil and
            self.inst.components.rider ~= nil and
            self.inst.components.rider:IsRiding() then
            return false, "MOUNTED"
        elseif recipe.level.ORPHANAGE > 0 and (
                self.inst.components.petleash == nil or
                self.inst.components.petleash:IsFull() or
                self.inst.components.petleash:HasPetWithTag("critter")
            ) then
            return false, "HASPET"
        elseif recipe.manufactured and (
                self.current_prototyper == nil or
                not self.current_prototyper:IsValid() or
                self.current_prototyper.components.prototyper == nil or
                not CanPrototypeRecipe(recipe.level, self.current_prototyper.components.prototyper.trees)
            ) then
            -- manufacturing stations requires the current active protyper in order to work
            return false
        end

        if recipe.canbuild ~= nil then
			local success, msg = recipe.canbuild(recipe, self.inst, pt, rotation, self.current_prototyper)
			if not success then
				return false, msg
			end
		end

		local is_buffered_build = self.buffered_builds[recname] ~= nil
        if is_buffered_build then
            self.buffered_builds[recname] = nil
            self.inst.replica.builder:SetIsBuildBuffered(recname, false)
        end

        if self.inst:HasTag("hungrybuilder") and not self.inst.sg:HasStateTag("slowaction") and not self.inst.sg:HasStateTag("giving") then
            local t = GetTime()
            local hasTimeExpired = self.last_hungry_build == nil or t > self.last_hungry_build + TUNING.HUNGRY_BUILDER_RESET_TIME
            local hasPlayerMoved = self.last_hungry_build_pt == nil or CheckHungryDistance(self.inst, self.last_hungry_build_pt)
            if hasTimeExpired and hasPlayerMoved then
                self.inst.sg.mem.dohungryfastbuildtalk = true
                self.inst.components.hunger:DoDelta(TUNING.HUNGRY_BUILDER_DELTA)
                self.inst:PushEvent("hungrybuild")
            else
                self.inst.sg.mem.dohungryfastbuildtalk = nil
            end
            self.last_hungry_build = t
            self.last_hungry_build_pt = self.inst:GetPosition()
        end

        self.inst:PushEvent("refreshcrafting")

		if recipe.manufactured then
			local materials, discounted = self:GetIngredients(recname)
            if self:CheckIngredientsForMimic(materials) then
                return false, "ITEMMIMIC"
            end

			self:RemoveIngredients(materials, recname, discounted)
			   -- its up to the prototyper to implement onactivate and handle spawning the prefab
		   return true
		end

        local prod = SpawnPrefab(recipe.product, recipe.chooseskin or skin, nil, self.inst.userid) or nil
        if prod ~= nil then
            pt = pt or self.inst:GetPosition()

            if prod.components.inventoryitem ~= nil then
                if self.inst.components.inventory ~= nil then
					local materials, discounted = self:GetIngredients(recname)
                    if self:CheckIngredientsForMimic(materials) then
                        return false, "ITEMMIMIC"
                    end

					local wetlevel = self:GetIngredientWetness(materials)
					if wetlevel > 0 and prod.components.inventoryitem ~= nil then
						prod.components.inventoryitem:InheritMoisture(wetlevel, self.inst:GetIsWet())
					end

					if prod.onPreBuilt ~= nil then
						prod:onPreBuilt(self.inst, materials, recipe)
					end

					self:RemoveIngredients(materials, recname, discounted)

                    --self.inst.components.inventory:GiveItem(prod)
                    self.inst:PushEvent("builditem", { item = prod, recipe = recipe, skin = skin, prototyper = self.current_prototyper })
                    if self.current_prototyper ~= nil and self.current_prototyper:IsValid() then
                        self.current_prototyper:PushEvent("builditem", { item = prod, recipe = recipe, skin = skin }) -- added this back for the gorge.
                    end
                    ProfileStatsAdd("build_"..prod.prefab)

					local numtogive = recipe.override_numtogive_fn and recipe.override_numtogive_fn(recipe, self.inst) or recipe.numtogive

                    if prod.components.equippable ~= nil
						and not recipe.dropitem
                        and self.inst.components.inventory:GetEquippedItem(prod.components.equippable.equipslot) == nil
                        and not prod.components.equippable:IsRestricted(self.inst) then
                        if numtogive <= 1 then
                            --The item is equippable. Equip it.
                            self.inst.components.inventory:Equip(prod)
                        elseif prod.components.stackable ~= nil then
                            --The item is stackable. Just increase the stack size of the original item.
                            prod.components.stackable:SetStackSize(numtogive)
                            self.inst.components.inventory:Equip(prod)
                        else
                            --We still need to equip the original product that was spawned, so do that.
                            self.inst.components.inventory:Equip(prod)
                            --Now spawn in the rest of the items and give them to the player.
                            for i = 2, numtogive do
                                local addt_prod = SpawnPrefab(recipe.product)
                                self.inst.components.inventory:GiveItem(addt_prod, nil, pt)
                            end
                        end
                    elseif numtogive <= 1 then
                        --Only the original item is being received.
						GiveOrDropItem(self, recipe, prod, pt)
                    elseif prod.components.stackable ~= nil then
                        --The item is stackable. Just increase the stack size of the original item.
                        prod.components.stackable:SetStackSize(numtogive)
						GiveOrDropItem(self, recipe, prod, pt)
                    else
                        --We still need to give the player the original product that was spawned, so do that.
						GiveOrDropItem(self, recipe, prod, pt)
                        --Now spawn in the rest of the items and give them to the player.
                        for i = 2, numtogive do
                            local addt_prod = SpawnPrefab(recipe.product)
							GiveOrDropItem(self, recipe, addt_prod, pt)
                        end
                    end

                    NotifyPlayerProgress("TotalItemsCrafted", 1, self.inst)

                    if self.onBuild ~= nil then
                        self.onBuild(self.inst, prod)
                    end
                    prod:OnBuilt(self.inst) -- This is an EntityScript function, custom logic should be defined in prod.OnBuiltFn.

                    return true
				else
					prod:Remove()
					prod = nil
                end
            else
				if not is_buffered_build then -- items that have intermediate build items (like statues)
					local materials, discounted = self:GetIngredients(recname)
                    if self:CheckIngredientsForMimic(materials) then
                        return false, "ITEMMIMIC"
                    end

					self:RemoveIngredients(materials, recname, discounted)
				end

                local spawn_pos = pt

                -- If a non-inventoryitem recipe specifies dropitem, position the created object
                -- away from the builder so that they don't overlap.
                if recipe.dropitem then
                    local angle = (self.inst.Transform:GetRotation() + GetRandomMinMax(-65, 65)) * DEGREES
                    local r = prod:GetPhysicsRadius(0.5) + self.inst:GetPhysicsRadius(0.5) + 0.1
                    spawn_pos = Vector3(
                        spawn_pos.x + r * math.cos(angle),
                        spawn_pos.y,
                        spawn_pos.z - r * math.sin(angle)
                    )
                end

                prod.Transform:SetPosition(spawn_pos:Get())
                --V2C: or 0 check added for backward compatibility with mods that
                --     have not been updated to support placement rotation yet
                prod.Transform:SetRotation(rotation or 0)
                self.inst:PushEvent("buildstructure", { item = prod, recipe = recipe, skin = skin })
                prod:PushEvent("onbuilt", { builder = self.inst, pos = pt })
                ProfileStatsAdd("build_"..prod.prefab)
                NotifyPlayerProgress("TotalItemsCrafted", 1, self.inst)

                if self.onBuild ~= nil then
                    self.onBuild(self.inst, prod)
                end

                prod:OnBuilt(self.inst) -- This is an EntityScript function, custom logic should be defined in prod.OnBuiltFn.

                return true
            end
        end
    end
end




--参数:
    --recipe: 字符串或配方对象,必需参数，表示要检查的配方，可以是配方名称字符串或已加载的配方对象
    --ignore_tempbonus: 布尔(boolean)类型，可选参数，默认值为false。决定是否计算临时科技加成
    --cached_tech_trees: 表格(table)类型，可选参数，默认值为nil。存储已计算过的配方科技等级结果，提高性能
--返回值:
    --布尔(boolean)类型，表示玩家是否知道/能够制作此配方
--功能：
    --检查玩家是否知道给定的配方。
--流程：
    -- 1.如果输入是字符串，转换为配方对象
    -- 2.如果配方不存在，返回false
    -- 3. 如果玩家处于自由建造模式，返回true
    -- 4. 检查配方的建造者标签要求：
        -- 如果配方需要特定标签但玩家没有，返回false
        -- 如果配方禁止特定标签但玩家有，返回false
    -- 5. 检查技能树要求：
        -- 如果配方需要特定技能但玩家未激活，返回false
        -- 如果配方禁止特定技能但玩家已激活，返回false
    -- 6. 检查配方是否在站台配方列表或已解锁配方列表中，返回true
    -- 7. 检查缓存的科技树结果，如果已有结果直接返回
    -- 8. 检查所有可用科技类型：
        -- 计算玩家的科技加成(永久加成加临时加成)
        -- 如果任一科技等级要求高于玩家当前等级，返回false
    -- 9. 更新缓存并返回true
function Builder:KnowsRecipe(recipe, ignore_tempbonus, cached_tech_trees)
    if type(recipe) == "string" then
		recipe = GetValidRecipe(recipe)
	end
    if recipe == nil then
        return false
	elseif self.freebuildmode then
		return true
	end

	--the following builder_tag/skill checks are require due to character swapping
	if (recipe.builder_tag and not self.inst:HasTag(recipe.builder_tag)) or
		(recipe.no_builder_tag and self.inst:HasTag(recipe.no_builder_tag))
	then
		return false
	end
	local skilltreeupdater = self.inst.components.skilltreeupdater
	if (recipe.builder_skill and not (skilltreeupdater and skilltreeupdater:IsActivated(recipe.builder_skill))) or
		(recipe.no_builder_skill and skilltreeupdater and skilltreeupdater:IsActivated(recipe.no_builder_skill))
	then
		return false
	end
	--

	if self.station_recipes[recipe.name] or table.contains(self.recipes, recipe.name) then
		return true
	end

    if cached_tech_trees and cached_tech_trees[recipe.level] ~= nil then
        return cached_tech_trees[recipe.level]
    end
    for i, v in ipairs(TechTree.AVAILABLE_TECH) do
        local bonusvalue = (self[TechTree.AVAILABLE_TECH_BONUS[v] or string.lower(v).."_bonus"] or 0)
        if not ignore_tempbonus then
            bonusvalue = bonusvalue + (self[TechTree.AVAILABLE_TECH_TEMPBONUS[v] or string.lower(v).."_tempbonus"] or 0)
        end
        if recipe.level[v] > bonusvalue then
            if cached_tech_trees then
                cached_tech_trees[recipe.level] = false
            end
            return false
        end
    end

    if cached_tech_trees then
        cached_tech_trees[recipe.level] = true
    end
    return true
end





--参数:
    --recipe: 字符串或配方对象,必需参数，指定要检查的配方
--返回值:
    --布尔(boolean)类型，表示玩家是否拥有制作此配方所需的全部材料
--功能：
    --检查玩家是否拥有制作某个配方所需的全部材料。
--流程：
    -- 1.如果输入是字符串，转换为配方对象
    -- 2.如果配方不存在，返回false
    -- 3.如果玩家处于自由建造模式，返回true
    -- 4. 检查普通材料：
        -- 遍历配方中的每个材料
        -- 计算实际需要的数量(考虑材料减免)
        -- 检查玩家背包中是否有足够数量
    -- 5. 检查角色材料(如生命值、理智值等)：
        -- 调用HasCharacterIngredient方法验证
    -- 6. 检查科技材料：
        -- 调用HasTechIngredient方法验证
    -- 7. 所有检查通过返回true，否则返回false
function Builder:HasIngredients(recipe)
    if type(recipe) == "string" then 
		recipe = GetValidRecipe(recipe)
	end
	if recipe ~= nil then
		if self.freebuildmode then
			return true
		end
		for i, v in ipairs(recipe.ingredients) do
            if not self.inst.components.inventory:Has(v.type, math.max(1, RoundBiasedUp(v.amount * self.ingredientmod)), true) then
				return false
			end
		end
		for i, v in ipairs(recipe.character_ingredients) do
			if not self:HasCharacterIngredient(v) then
				return false
			end
		end
		for i, v in ipairs(recipe.tech_ingredients) do
			if not self:HasTechIngredient(v) then
				return false
			end
		end
		return true
	end

	return false
end



--该函数已废弃，内容重定向到HasIngredients
--参数:
    --recipe_name: 字符串类型，必填，表示配方的名称。
--返回值:
    --布尔(boolean)类型，表示玩家是否拥有制作此配方所需的全部材料
--功能：
    --判断玩家是否拥有制作指定配方所需的全部材料。
--流程：
    -- 1.调用GetValidRecipe(recipe_name)获取有效的配方对象
    -- 2.调用self:HasIngredients()方法检查玩家是否拥有所需材料
function Builder:CanBuild(recipe_name)
	return self:HasIngredients(GetValidRecipe(recipe_name))
end





--参数:
    --recname: 字符串类型，必填，表示配方的名称。
--返回值:
    --布尔(boolean)类型，表示玩家是否可以学习此配方。
--功能：
    --判断玩家是否能够学习特定的配方，主要检查角色标签和技能树要求。
--流程：
    --1.调用GetValidRecipe(recname)获取有效的配方对象
    --2.如果配方不存在，返回false
    --3.检查角色标签要求：
        -- 如果配方要求特定标签(builder_tag)但角色没有该标签，返回false
        -- 如果配方指定排除标签(no_builder_tag)且角色有该标签，返回false
    --4.检查技能树要求：
        -- 获取角色的技能树更新器组件
        -- 如果配方要求特定技能(builder_skill)但角色未激活该技能，返回false
        -- 如果配方指定排除技能(no_builder_skill)且角色已激活该技能，返回false
    --5.通过所有检查后返回true
function Builder:CanLearn(recname)
	local recipe = GetValidRecipe(recname)
	if recipe == nil then
		return false
	elseif (recipe.builder_tag and not self.inst:HasTag(recipe.builder_tag)) or
		(recipe.no_builder_tag and self.inst:HasTag(recipe.no_builder_tag))
	then
		return false
	end
	local skilltreeupdater = self.inst.components.skilltreeupdater
	if (recipe.builder_skill and not (skilltreeupdater and skilltreeupdater:IsActivated(recipe.builder_skill))) or
		(recipe.no_builder_skill and skilltreeupdater and skilltreeupdater:IsActivated(recipe.no_builder_skill))
	then
		return false
	end
	return true
end






--参数:
    --dt: 数值类型，必填，表示经过的时间（单位：秒）。
--返回值:
    --无返回值
--功能:
    --处理游戏长时间更新（通常发生在游戏加载或时间快进时）时对构建器组件状态的更新。
--流程：
    -- 1.检查self.last_hungry_build是否存在（即玩家是否曾经以饥饿模式建造过）
    -- 2.如果存在，则将self.last_hungry_build减去传入的时间值dt
    -- 3.这样可以确保在游戏长时间更新后，饥饿建造的计时器能正确反映已经过去的时间
function Builder:LongUpdate(dt)
    if self.last_hungry_build ~= nil then
        self.last_hungry_build = self.last_hungry_build - dt
    end
end

--------------------------------------------------------------------------
--RPC handlers
--------------------------------------------------------------------------


--参数:
    --self: Builder组件实例，必需参数，无默认值。
    --ing_recipe: table,原料配方对象，表示要尝试制作的原料配方,必须参数，无默认值
--返回值:
    --布尔值(boolean)，表示尝试制作是否成功,默认false
    --布尔值(boolean)，表示玩家是否可以接触/使用这个配方（无论是否有足够材料）,默认false
--功能:
    --尝试制作一个配方的原料配方。当玩家想制作某个物品但缺少某种原料时，如果该原料本身也可以被制作，此函数会尝试先制作这个原料。
--流程：
    -- 1. 确定玩家的配方知识状态（不考虑临时科技加成）
    -- 2.检查玩家在不考虑临时科技加成的情况下是否可以原型化这个配方
    -- 3.检查玩家是否可以学习这个配方
    -- 4.判断是否在使用临时科技加成
    -- 5.处理两种主要情况：
        -- 1.如果玩家已知该配方：
            -- 设置canaccess为true
            -- 如果有足够材料，就制作它
            -- 制作完成后进行相应处理：
                -- 1.如使用了临时科技加成，则消耗它
                -- 2.处理自由建造模式下的原型化
                -- 3.对于通过临时科技加成知道的配方，可能解锁它
                -- 4.对于通过科技加成知道的配方，添加到已知配方列表
        -- 2.如果玩家不知道但可以学习且有足够科技等级：
            -- 设置canaccess为true
            -- 如果有足够材料，制作它并解锁该配方
    -- 6.返回制作结果和是否可访问的状态
local function _TryMakeIngredientRecipe(self, ing_recipe)
	--Need to determine this NOW before calling async MakeRecipe
	local knows_no_temp = self:KnowsRecipe(ing_recipe, true)
	local canproto_no_temp = CanPrototypeRecipe(ing_recipe.level, self.accessible_tech_trees_no_temp)
	local canlearn = self:CanLearn(ing_recipe.name)
	local usingtempbonus = not knows_no_temp and not canproto_no_temp
	local success, canaccess = false, false
	if self:KnowsRecipe(ing_recipe) then
		canaccess = true
		if self:HasIngredients(ing_recipe) then
			self:MakeRecipe(ing_recipe, nil, nil, ValidateRecipeSkinRequest(self.inst.userid, ing_recipe.product, nil),
				function()
					if usingtempbonus then
						self:ConsumeTempTechBonuses()
					end

					if self.freebuildmode then
						--V2C: free-build should still trigger prototyping
						if not table.contains(self.recipes, ing_recipe.name) and CanPrototypeRecipe(ing_recipe.level, self.accessible_tech_trees) then
							self:ActivateCurrentResearchMachine(ing_recipe)
						end
					elseif not knows_no_temp and canproto_no_temp and canlearn then
						--assert(not usingtempbonus) --sanity check
						--V2C: for recipes known through temp bonus buff,
						--     but can be prototyped without consuming it
						self:ActivateCurrentResearchMachine(ing_recipe)
						self:UnlockRecipe(ing_recipe.name)
					elseif not ing_recipe.nounlock then
						--V2C: for recipes known through tech bonus, still
						--     want to unlock in case we reroll characters
						self:AddRecipe(ing_recipe.name)
					else
						self:ActivateCurrentResearchMachine(ing_recipe)
					end
				end
			)
			success = true
		end
	elseif canlearn and CanPrototypeRecipe(ing_recipe.level, self.accessible_tech_trees) then
		canaccess = true
		if self:HasIngredients(ing_recipe) then
			self:MakeRecipe(ing_recipe, nil, nil, ValidateRecipeSkinRequest(self.inst.userid, ing_recipe.product, nil),
				function()
					if usingtempbonus then
						self:ConsumeTempTechBonuses()
					end
					self:ActivateCurrentResearchMachine(ing_recipe)
					self:UnlockRecipe(ing_recipe.name)
				end
			)
			success = true
		end
	end
	return success, canaccess
end




--参数:
    --recipe: table,必需参数，配方对象，包含制作物品所需的全部信息（材料、科技需求等）
    --skin: string类型，可选参数，默认值nil，皮肤ID，决定制作出的物品使用什么外观皮肤
--返回值:
    --无返回值
--功能:
    --该函数用于从游戏的制作菜单中制作物品。当玩家在制作菜单中选择制作一个物品时，会调用此函数来处理制作过程
--流程：
-- 1.首先检查玩家的背包是否由玩家自己打开，如果背包被游戏隐藏则不允许制作
-- 2.检查玩家是否有足够的材料制作该物品
    -- 1.如果有足够材料，并且配方不需要放置器（placer为nil），则会尝试制作：
        -- 1.确定玩家在没有临时科技加成的情况下是否知道该配方
        -- 2.确定玩家在没有临时科技加成的情况下是否可以原型化该配方
        -- 3.确定玩家是否可以学习该配方
        -- 4.确定是否需要使用临时科技加成
        -- 5.如果玩家已知该配方，调用MakeRecipe制作物品，并在成功后：
            -- 1.如果使用了临时科技加成，消耗这些加成
            -- 2.处理自由建造模式下的原型化逻辑
            -- 3.如果需要，解锁或添加配方到已知列表
            -- 4.激活当前的研究机器（如果需要）
        -- 6.如果玩家不知道配方但可以学习且可以原型化，调用MakeRecipe制作物品，并在成功后激活研究机器并解锁配方
    -- 2.如果没有足够材料，则会尝试自动制作缺少的材料：
        -- 1.遍历配方中的所有材料
        -- 2.对于玩家没有足够数量的材料，查找该材料的制作配方
        -- 3.尝试制作这种材料
        -- 4.如果无法访问材料的配方且该材料有前向材料(forward_ingredients)，尝试制作前向材料
function Builder:MakeRecipeFromMenu(recipe, skin)
    if not self.inst.components.inventory:IsOpenedBy(self.inst) then
        return -- NOTES(JBK): The inventory was hidden by gameplay do not allow crafting.
    end
    if self:HasIngredients(recipe) then
		if recipe.placer == nil then
			--Need to determine this NOW before calling async MakeRecipe
			local knows_no_temp = self:KnowsRecipe(recipe, true)
			local canproto_no_temp = CanPrototypeRecipe(recipe.level, self.accessible_tech_trees_no_temp)
			local canlearn = self:CanLearn(recipe.name)
			local usingtempbonus = not knows_no_temp and not canproto_no_temp

			if self:KnowsRecipe(recipe) then
                self:MakeRecipe(recipe, nil, nil, ValidateRecipeSkinRequest(self.inst.userid, recipe.product, skin),
                    function()
						if usingtempbonus then
							self:ConsumeTempTechBonuses()
						end

                        if self.freebuildmode then
                            --V2C: free-build should still trigger prototyping
                            if not table.contains(self.recipes, recipe.name) and CanPrototypeRecipe(recipe.level, self.accessible_tech_trees) then
                                self:ActivateCurrentResearchMachine(recipe)
                            end
						elseif not knows_no_temp and canproto_no_temp and canlearn then
							--assert(not usingtempbonus) --sanity check
							--V2C: for recipes known through temp bonus buff,
							--     but can be prototyped without consuming it
							self:ActivateCurrentResearchMachine(recipe)
							self:UnlockRecipe(recipe.name)
                        elseif not recipe.nounlock then
                            --V2C: for recipes known through tech bonus, still
                            --     want to unlock in case we reroll characters
                            self:AddRecipe(recipe.name)
						else
							self:ActivateCurrentResearchMachine(recipe)
                        end

                    end
                )
			elseif canlearn and CanPrototypeRecipe(recipe.level, self.accessible_tech_trees) then
				self:MakeRecipe(recipe, nil, nil, ValidateRecipeSkinRequest(self.inst.userid, recipe.product, skin),
					function()
						if usingtempbonus then
							self:ConsumeTempTechBonuses()
						end
						self:ActivateCurrentResearchMachine(recipe)
						self:UnlockRecipe(recipe.name)
					end
				)
			end
        end
	else
		for i, ing in ipairs(recipe.ingredients) do
			local ing_recipe = GetValidRecipe(ing.type)
			if ing_recipe and not self.inst.components.inventory:Has(ing.type, math.max(1, RoundBiasedUp(ing.amount * self.ingredientmod)), true) then
				local success, canaccess = _TryMakeIngredientRecipe(self, ing_recipe)
				if not canaccess and ing_recipe.forward_ingredients then
					--V2C: skill tree might've locked basic ingredient recipe. try the forwarded ingredient recipes.
					for i, v in ipairs(ing_recipe.forward_ingredients) do
						ing_recipe = GetValidRecipe(v)
						if ing_recipe then
							success, canaccess = _TryMakeIngredientRecipe(self, ing_recipe)
							if canaccess then
								break
							end
						end
					end
				end
			end
		end
    end
end




--参数:
    --recipe: table,必需参数，配方对象，包含制作物品所需的全部信息
    --pt: Vector3类型，必需参数，表示在游戏世界中放置物品的坐标位置
    --rot: number，必需参数，表示物品放置时的朝向角度
    --skin: string类型，可选参数，默认值nil，皮肤ID，决定制作出的物品使用什么外观皮肤
--返回值:
    --无返回值
--功能:
    --在游戏世界的指定位置点制作并放置一个已缓冲的物品或建筑结构。
    --该函数是建造系统中实现将预制好的建筑物放置到世界特定位置的关键方法。
--流程：
    -- 1.首先检查玩家的物品栏是否由玩家自己打开，如果未打开则直接返回
    -- 2.然后检查以下所有条件是否同时满足：
        -- 1.配方必须有放置器(placer)属性
        -- 2.玩家必须知道这个配方(KnowsRecipe)
        -- 3.该建造必须已经被缓冲(IsBuildBuffered)
        -- 4.世界地图必须允许在指定位置部署该配方(CanDeployRecipeAtPoint)
    --3.如果所有条件都满足，调用MakeRecipe函数在指定位置和旋转角度制作并放置物品
function Builder:MakeRecipeAtPoint(recipe, pt, rot, skin)
    if not self.inst.components.inventory:IsOpenedBy(self.inst) then
        return -- NOTES(JBK): The inventory was hidden by gameplay do not allow crafting.
    end

    if recipe.placer ~= nil and
        self:KnowsRecipe(recipe) and
        self:IsBuildBuffered(recipe.name) and
        TheWorld.Map:CanDeployRecipeAtPoint(pt, recipe, rot) then
        self:MakeRecipe(recipe, pt, rot, skin)
    end
end





--参数:
    --recname: string类型，必填，表示要缓冲的配方名称
--返回值:
    --无返回值
--功能:
    --用于将一个可建造的配方添加到"缓冲构建"列表中。
    --在游戏中，当玩家选择放置结构时，该函数允许预先消耗材料，然后再寻找合适位置放置该结构。
--流程：
    -- 1.检查玩家物品栏是否由玩家自己打开，若未打开则退出函数
    -- 2.获取有效的配方对象(GetValidRecipe(recname))
    -- 3.验证配方是否满足条件：
        -- 配方存在(recipe ~= nil)
        -- 配方有放置器(recipe.placer ~= nil)
        -- 该配方尚未被缓冲(not self:IsBuildBuffered(recname))
        -- 玩家拥有足够材料(self:HasIngredients(recipe))
    -- 4.检查玩家对配方的掌握状态：
        -- 计算knows_no_temp：不考虑临时科技加成时是否掌握配方
        -- 计算canproto_no_temp：不考虑临时科技加成时是否能原型化
        -- 计算canlearn：玩家是否能学习该配方
        -- 确定是否使用临时科技加成(usingtempbonus)
    -- 5.处理玩家已知配方的情况：
        -- 必要时消耗临时科技加成
        -- 处理自由构建模式情况
        -- 处理可能需要激活研究机器的情况
        -- 必要时解锁或添加配方
    -- 6.处理玩家不知道但可以学习的配方：
        -- 激活研究机器
        -- 解锁配方
    -- 7.获取并检查材料：
        -- 提取所需材料和折扣信息
        -- 检查材料中是否有会模仿的物品(itemmimic组件,猜测是表面看上去是配方材料,调用制作后会触发组件效果从而提前结束BufferBuild)
    -- 8.从玩家库存中移除所需材料
    -- 9.更新构建状态：
        -- 将配方添加到缓冲构建列表中
        -- 通知客户端该配方已被缓冲构建
function Builder:BufferBuild(recname)
    if not self.inst.components.inventory:IsOpenedBy(self.inst) then
        return -- NOTES(JBK): The inventory was hidden by gameplay do not allow crafting.
    end

    local recipe = GetValidRecipe(recname)
    if recipe ~= nil and recipe.placer ~= nil and not self:IsBuildBuffered(recname) and self:HasIngredients(recipe) then
		local knows_no_temp = self:KnowsRecipe(recipe, true)
		local canproto_no_temp = CanPrototypeRecipe(recipe.level, self.accessible_tech_trees_no_temp)
		local canlearn = self:CanLearn(recname)
		local usingtempbonus = not knows_no_temp and not canproto_no_temp

        if self:KnowsRecipe(recipe) then
			if usingtempbonus then
				self:ConsumeTempTechBonuses()
			end

            if self.freebuildmode then
                --V2C: free-build should still trigger prototyping
                if not table.contains(self.recipes, recname) and CanPrototypeRecipe(recipe.level, self.accessible_tech_trees) then
					-- Note:	This can currently activate prototypers that have no relation to the item or structure
					--			built, such as when building a Fire Pit near a Science Machine or Mad Science Lab.
                    self:ActivateCurrentResearchMachine(recipe)
                end
			elseif not knows_no_temp and canproto_no_temp and canlearn then
				--assert(not usingtempbonus) --sanity check
				--V2C: for recipes known through temp bonus buff,
				--     but can be prototyped without consuming it
				self:ActivateCurrentResearchMachine(recipe)
				self:UnlockRecipe(recname)
            elseif not recipe.nounlock then
                --V2C: for recipes known through tech bonus, still
                --     want to unlock in case we reroll characters
                self:AddRecipe(recname)
            end
		elseif canlearn and CanPrototypeRecipe(recipe.level, self.accessible_tech_trees) then
			if usingtempbonus then
				self:ConsumeTempTechBonuses()
			end
			self:ActivateCurrentResearchMachine(recipe)
			self:UnlockRecipe(recname)
		else
			return
		end

        local materials, discounted = self:GetIngredients(recname)
        if self:CheckIngredientsForMimic(materials) then
            return
        end

        self:RemoveIngredients(materials, recname, discounted)
        self.buffered_builds[recname] = true
        self.inst.replica.builder:SetIsBuildBuffered(recname, true)
    end
end



return Builder




